/*
 * 86Box    A hypervisor and IBM PC system emulator that specializes in
 *          running old operating systems and software designed for IBM
 *          PC systems and compatibles from 1981 through fairly recent
 *          system designs based on the PCI bus.
 *
 *          This file is part of the 86Box distribution.
 *
 *          Platform main support module for Windows.
 *
 *
 *
 * Authors: Sarah Walker, <https://pcem-emulator.co.uk/>
 *          Miran Grca, <mgrca8@gmail.com>
 *          Fred N. van Kempen, <decwiz@yahoo.com>
 *
 *          Copyright 2008-2019 Sarah Walker.
 *          Copyright 2016-2019 Miran Grca.
 *          Copyright 2017-2019 Fred N. van Kempen.
 *          Copyright 2021 Laci bá'
 *          Copyright 2021-2023 Jasmine Iwanek.
 */
#define UNICODE
#define NTDDI_VERSION 0x06010000
#include <windows.h>
#include <commctrl.h>
#include <shlobj.h>
#include <shobjidl.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <direct.h>
#include <wchar.h>
#include <io.h>
#include <stdatomic.h>
#define HAVE_STDARG_H
#include <86box/86box.h>
#include <86box/config.h>
#include <86box/device.h>
#include <86box/keyboard.h>
#include <86box/mouse.h>
#include <86box/timer.h>
#include <86box/nvr.h>
#include <86box/video.h>
#include <86box/mem.h>
#include <86box/rom.h>
#include <86box/path.h>
#define GLOBAL
#include <86box/plat.h>
#include <86box/plat_dynld.h>
#include <86box/thread.h>
#include <86box/ui.h>
#ifdef USE_VNC
#    include <86box/vnc.h>
#endif
#include <86box/win_sdl.h>
#include <86box/win_opengl.h>
#include <86box/win.h>
#include <86box/version.h>
#include <86box/gdbstub.h>
#ifdef MTR_ENABLED
#    include <minitrace/minitrace.h>
#endif

typedef struct rc_str_t {
    WCHAR str[1024];
} rc_str_t;

/* Platform Public data, specific. */
HINSTANCE    hinstance; /* application instance */
HANDLE       ghMutex;
uint32_t     lang_id;  /* current and system language ID */
uint32_t     lang_sys; /* current and system language ID */
DWORD        dwSubLangID;
int          acp_utf8; /* Windows supports UTF-8 codepage */
volatile int cpu_thread_run = 1;

/* Local data. */
static HANDLE        thMain;
static rc_str_t     *lpRCstr2048    = NULL;
static rc_str_t     *lpRCstr4096    = NULL;
static rc_str_t     *lpRCstr4352    = NULL;
static rc_str_t     *lpRCstr4608    = NULL;
static rc_str_t     *lpRCstr5120    = NULL;
static rc_str_t     *lpRCstr5376    = NULL;
static rc_str_t     *lpRCstr5632    = NULL;
static rc_str_t     *lpRCstr5888    = NULL;
static rc_str_t     *lpRCstr6144    = NULL;
static rc_str_t     *lpRCstr7168    = NULL;
static int           vid_api_inited = 0;
static char         *argbuf;
static int           first_use = 1;
static LARGE_INTEGER StartingTime;
static LARGE_INTEGER Frequency;

static const struct {
    const char *name;
    int         local;
    int (*init)(void *);
    void (*close)(void);
    void (*resize)(int x, int y);
    int (*pause)(void);
    void (*enable)(int enable);
    void (*set_fs)(int fs);
    void (*reload)(void);
} vid_apis[RENDERERS_NUM] = {
    { "SDL_Software", 1, (int (*)(void *)) sdl_inits, sdl_close, NULL, sdl_pause, sdl_enable, sdl_set_fs, sdl_reload },
    { "SDL_Hardware", 1, (int (*)(void *)) sdl_inith, sdl_close, NULL, sdl_pause, sdl_enable, sdl_set_fs, sdl_reload },
    { "SDL_OpenGL", 1, (int (*)(void *)) sdl_initho, sdl_close, NULL, sdl_pause, sdl_enable, sdl_set_fs, sdl_reload },
    { "OpenGL_Core", 1, (int (*)(void *)) opengl_init, opengl_close, opengl_resize, opengl_pause, NULL, opengl_set_fs, opengl_reload }
#ifdef USE_VNC
    ,
    { "VNC", 0, vnc_init, vnc_close, vnc_resize, vnc_pause, NULL, NULL }
#endif
};

extern int title_update;

#ifdef ENABLE_WIN_LOG
int win_do_log = ENABLE_WIN_LOG;

static void
win_log(const char *fmt, ...)
{
    va_list ap;

    if (win_do_log) {
        va_start(ap, fmt);
        pclog_ex(fmt, ap);
        va_end(ap);
    }
}
#else
#    define win_log(fmt, ...)
#endif

void
free_string(rc_str_t **str)
{
    if (*str != NULL) {
        free(*str);
        *str = NULL;
    }
}

static void
LoadCommonStrings(void)
{
    int i;

    free_string(&lpRCstr7168);
    free_string(&lpRCstr6144);
    free_string(&lpRCstr5888);
    free_string(&lpRCstr5632);
    free_string(&lpRCstr5376);
    free_string(&lpRCstr5120);
    free_string(&lpRCstr4608);
    free_string(&lpRCstr4352);
    free_string(&lpRCstr4096);
    free_string(&lpRCstr2048);

    lpRCstr2048 = calloc(STR_NUM_2048, sizeof(rc_str_t));
    lpRCstr4096 = calloc(STR_NUM_4096, sizeof(rc_str_t));
    lpRCstr4352 = calloc(STR_NUM_4352, sizeof(rc_str_t));
    lpRCstr4608 = calloc(STR_NUM_4608, sizeof(rc_str_t));
    lpRCstr5120 = calloc(STR_NUM_5120, sizeof(rc_str_t));
    lpRCstr5376 = calloc(STR_NUM_5376, sizeof(rc_str_t));
    lpRCstr5632 = calloc(STR_NUM_5632, sizeof(rc_str_t));
    lpRCstr5888 = calloc(STR_NUM_5888, sizeof(rc_str_t));
    lpRCstr6144 = calloc(STR_NUM_6144, sizeof(rc_str_t));
    lpRCstr7168 = calloc(STR_NUM_7168, sizeof(rc_str_t));

    for (i = 0; i < STR_NUM_2048; i++)
        LoadString(hinstance, 2048 + i, lpRCstr2048[i].str, 1024);

    for (i = 0; i < STR_NUM_4096; i++)
        LoadString(hinstance, 4096 + i, lpRCstr4096[i].str, 1024);

    for (i = 0; i < STR_NUM_4352; i++)
        LoadString(hinstance, 4352 + i, lpRCstr4352[i].str, 1024);

    for (i = 0; i < STR_NUM_4608; i++)
        LoadString(hinstance, 4608 + i, lpRCstr4608[i].str, 1024);

    for (i = 0; i < STR_NUM_5120; i++)
        LoadString(hinstance, 5120 + i, lpRCstr5120[i].str, 1024);

    for (i = 0; i < STR_NUM_5376; i++) {
        if ((i == 0) || (i > 3))
            LoadString(hinstance, 5376 + i, lpRCstr5376[i].str, 1024);
    }

    for (i = 0; i < STR_NUM_5632; i++) {
        if ((i == 0) || (i > 3))
            LoadString(hinstance, 5632 + i, lpRCstr5632[i].str, 1024);
    }

    for (i = 0; i < STR_NUM_5888; i++)
        LoadString(hinstance, 5888 + i, lpRCstr5888[i].str, 1024);

    for (i = 0; i < STR_NUM_6144; i++)
        LoadString(hinstance, 6144 + i, lpRCstr6144[i].str, 1024);

    for (i = 0; i < STR_NUM_7168; i++)
        LoadString(hinstance, 7168 + i, lpRCstr7168[i].str, 1024);
}

size_t
mbstoc16s(uint16_t dst[], const char src[], int len)
{
    if (src == NULL)
        return 0;
    if (len < 0)
        return 0;

    size_t ret = MultiByteToWideChar(CP_UTF8, 0, src, -1, dst, dst == NULL ? 0 : len);

    if (!ret) {
        return -1;
    }

    return ret;
}

size_t
c16stombs(char dst[], const uint16_t src[], int len)
{
    if (src == NULL)
        return 0;
    if (len < 0)
        return 0;

    size_t ret = WideCharToMultiByte(CP_UTF8, 0, src, -1, dst, dst == NULL ? 0 : len, NULL, NULL);

    if (!ret) {
        return -1;
    }

    return ret;
}

int
has_language_changed(uint32_t id)
{
    return (lang_id != id);
}

/* Set (or re-set) the language for the application. */
void
set_language(uint32_t id)
{
    if (id == 0xFFFF) {
        set_language(lang_sys);
        lang_id = id;
        return;
    }

    if (lang_id != id) {
        /* Set our new language ID. */
        lang_id = id;
        SetThreadUILanguage(lang_id);

        /* Load the strings table for this ID. */
        LoadCommonStrings();

        /* Reload main menu */
        menuMain = LoadMenu(hinstance, L"MainMenu");
        if (hwndMain != NULL)
            SetMenu(hwndMain, menuMain);

        /* Re-init all the menus */
        ResetAllMenus();
        media_menu_init();
    }
}

wchar_t *
plat_get_string(int i)
{
    LPTSTR str;

    if ((i >= 2048) && (i <= 3071))
        str = lpRCstr2048[i - 2048].str;
    else if ((i >= 4096) && (i <= 4351))
        str = lpRCstr4096[i - 4096].str;
    else if ((i >= 4352) && (i <= 4607))
        str = lpRCstr4352[i - 4352].str;
    else if ((i >= 4608) && (i <= 5119))
        str = lpRCstr4608[i - 4608].str;
    else if ((i >= 5120) && (i <= 5375))
        str = lpRCstr5120[i - 5120].str;
    else if ((i >= 5376) && (i <= 5631))
        str = lpRCstr5376[i - 5376].str;
    else if ((i >= 5632) && (i <= 5887))
        str = lpRCstr5632[i - 5632].str;
    else if ((i >= 5888) && (i <= 6143))
        str = lpRCstr5888[i - 5888].str;
    else if ((i >= 6144) && (i <= 7167))
        str = lpRCstr6144[i - 6144].str;
    else
        str = lpRCstr7168[i - 7168].str;

    return str;
}

#ifdef MTR_ENABLED
void
init_trace(void)
{
    mtr_init("trace.json");
    mtr_start();
}

void
shutdown_trace(void)
{
    mtr_stop();
    mtr_shutdown();
}
#endif

/* Create a console if we don't already have one. */
static void
CreateConsole(int init)
{
    HANDLE h;
    FILE  *fp;
    fpos_t p;
    int    i;

    if (!init) {
        if (force_debug)
            FreeConsole();
        return;
    }

    /* Are we logging to a file? */
    p = 0;
    (void) fgetpos(stdout, &p);
    if (p != -1)
        return;

    /* Not logging to file, attach to console. */
    if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
        /* Parent has no console, create one. */
        if (!AllocConsole()) {
            /* Cannot create console, just give up. */
            return;
        }
    }
    fp = NULL;
    if ((h = GetStdHandle(STD_OUTPUT_HANDLE)) != NULL) {
        /* We got the handle, now open a file descriptor. */
        if ((i = _open_osfhandle((intptr_t) h, _O_TEXT)) != -1) {
            /* We got a file descriptor, now allocate a new stream. */
            if ((fp = _fdopen(i, "w")) != NULL) {
                /* Got the stream, re-initialize stdout without it. */
                (void) freopen("CONOUT$", "w", stdout);
                setvbuf(stdout, NULL, _IONBF, 0);
                fflush(stdout);
            }
        }
    }

    if (fp != NULL) {
        fclose(fp);
        fp = NULL;
    }
}

static void
CloseConsole(void)
{
    CreateConsole(0);
}

/* Process the commandline, and create standard argc/argv array. */
static int
ProcessCommandLine(char ***argv)
{
    char **args;
    int    argc_max;
    int    i;
    int    q;
    int    argc;

    if (acp_utf8) {
        i      = strlen(GetCommandLineA()) + 1;
        argbuf = (char *) malloc(i);
        strcpy(argbuf, GetCommandLineA());
    } else {
        i      = c16stombs(NULL, GetCommandLineW(), 0) + 1;
        argbuf = (char *) malloc(i);
        c16stombs(argbuf, GetCommandLineW(), i);
    }

    argc     = 0;
    argc_max = 64;
    args     = (char **) malloc(sizeof(char *) * argc_max);
    if (args == NULL) {
        free(argbuf);
        return 0;
    }

    /* parse commandline into argc/argv format */
    i = 0;
    while (argbuf[i]) {
        while (argbuf[i] == ' ')
            i++;

        if (argbuf[i]) {
            if ((argbuf[i] == '\'') || (argbuf[i] == '"')) {
                q = argbuf[i++];
                if (!argbuf[i])
                    break;
            } else
                q = 0;

            args[argc++] = &argbuf[i];

            if (argc >= argc_max) {
                argc_max += 64;
                args = realloc(args, sizeof(char *) * argc_max);
                if (args == NULL) {
                    free(argbuf);
                    return 0;
                }
            }

            while ((argbuf[i]) && (q ? (argbuf[i] != q) : (argbuf[i] != ' ')))
                i++;

            if (argbuf[i]) {
                argbuf[i] = 0;
                i++;
            }
        }
    }

    args[argc] = NULL;
    *argv      = args;

    return argc;
}

/* For the Windows platform, this is the start of the application. */
int WINAPI
WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR lpszArg, int nCmdShow)
{
    char **argv = NULL;
    int    argc;
    int    i;

    /* Initialize the COM library for the main thread. */
    CoInitializeEx(NULL, COINIT_MULTITHREADED);

    /* Check if Windows supports UTF-8 */
    if (GetACP() == CP_UTF8)
        acp_utf8 = 1;
    else
        acp_utf8 = 0;

    /* Set this to the default value (windowed mode). */
    video_fullscreen = 0;

    /* We need this later. */
    hinstance = hInst;

    /* Set the application version ID string. */
    sprintf(emu_version, "%s v%s", EMU_NAME, EMU_VERSION_FULL);

    /* First, set our (default) language. */
    lang_sys = GetThreadUILanguage();
    set_language(DEFAULT_LANGUAGE);

    /* Process the command line for options. */
    argc = ProcessCommandLine(&argv);

    /* Pre-initialize the system, this loads the config file. */
    if (!pc_init(argc, argv)) {
        /* Detach from console. */
        if (force_debug)
            CreateConsole(0);

        if (source_hwnd)
            PostMessage((HWND) (uintptr_t) source_hwnd, WM_HAS_SHUTDOWN, (WPARAM) 0, (LPARAM) hwndMain);

        free(argbuf);
        free(argv);
        return 1;
    }

    extern int gfxcard[2];
    gfxcard[1] = 0;

    /* Create console window. */
    if (force_debug) {
        CreateConsole(1);
        atexit(CloseConsole);
    }

    /* Handle our GUI. */
    i = ui_init(nCmdShow);

    /* Uninitialize COM before exit. */
    CoUninitialize();

    free(argbuf);
    free(argv);
    return i;
}

void
main_thread(void *param)
{
    uint32_t old_time;
    uint32_t new_time;
    int      drawits;
    int      frames;

    framecountx  = 0;
    title_update = 1;
    old_time     = GetTickCount();
    drawits = frames = 0;
    while (!is_quit && cpu_thread_run) {
        /* See if it is time to run a frame of code. */
        new_time = GetTickCount();
#ifdef USE_GDBSTUB
        if (gdbstub_next_asap && (drawits <= 0))
            drawits = 10;
        else
#endif
            drawits += (new_time - old_time);
        old_time = new_time;
        if (drawits > 0 && !dopause) {
            /* Yes, so do one frame now. */
            drawits -= 10;
            if (drawits > 50)
                drawits = 0;

            /* Run a block of code. */
            pc_run();

            /* Every 200 frames we save the machine status. */
            if (++frames >= 200 && nvr_dosave) {
                nvr_save();
                nvr_dosave = 0;
                frames     = 0;
            }
        } else /* Just so we dont overload the host OS. */
            Sleep(1);

        /* If needed, handle a screen resize. */
        if (atomic_load(&doresize_monitors[0]) && !video_fullscreen && !is_quit) {
            if (vid_resize & 2)
                plat_resize(fixed_size_x, fixed_size_y);
            else
                plat_resize(scrnsz_x, scrnsz_y);
            atomic_store(&doresize_monitors[0], 0);
        }
    }

    is_quit = 1;
}

/*
 * We do this here since there is platform-specific stuff
 * going on here, and we do it in a function separate from
 * main() so we can call it from the UI module as well.
 */
void
do_start(void)
{
    LARGE_INTEGER qpc;

    /* We have not stopped yet. */
    is_quit = 0;

    /* Initialize the high-precision timer. */
    timeBeginPeriod(1);
    QueryPerformanceFrequency(&qpc);
    timer_freq = qpc.QuadPart;
    win_log("Main timer precision: %llu\n", timer_freq);

    /* Start the emulator, really. */
    thMain = thread_create(main_thread, NULL);
    SetThreadPriority(thMain, THREAD_PRIORITY_HIGHEST);
}

/* Cleanly stop the emulator. */
void
do_stop(void)
{
    /* Claim the video blitter. */
    startblit();

    vid_apis[vid_api].close();

    pc_close(thMain);

    thMain = NULL;

    if (source_hwnd)
        PostMessage((HWND) (uintptr_t) source_hwnd, WM_HAS_SHUTDOWN, (WPARAM) 0, (LPARAM) hwndMain);
}

void
plat_get_exe_name(char *s, int size)
{
    wchar_t *temp;

    if (acp_utf8)
        GetModuleFileNameA(hinstance, s, size);
    else {
        temp = malloc(size * sizeof(wchar_t));
        GetModuleFileNameW(hinstance, temp, size);
        c16stombs(s, temp, size);
        free(temp);
    }
}

void
plat_tempfile(char *bufp, char *prefix, char *suffix)
{
    SYSTEMTIME SystemTime;

    if (prefix != NULL)
        sprintf(bufp, "%s-", prefix);
    else
        strcpy(bufp, "");

    GetLocalTime(&SystemTime);
    sprintf(&bufp[strlen(bufp)], "%d%02d%02d-%02d%02d%02d-%03d%s",
            SystemTime.wYear, SystemTime.wMonth, SystemTime.wDay,
            SystemTime.wHour, SystemTime.wMinute, SystemTime.wSecond,
            SystemTime.wMilliseconds,
            suffix);
}

int
plat_getcwd(char *bufp, int max)
{
    wchar_t *temp;

    if (acp_utf8)
        (void) _getcwd(bufp, max);
    else {
        temp = malloc(max * sizeof(wchar_t));
        (void) _wgetcwd(temp, max);
        c16stombs(bufp, temp, max);
        free(temp);
    }

    return 0;
}

int
plat_chdir(char *path)
{
    wchar_t *temp;
    int      len;
    int      ret;

    if (acp_utf8)
        return (_chdir(path));
    else {
        len  = mbstoc16s(NULL, path, 0) + 1;
        temp = malloc(len * sizeof(wchar_t));
        mbstoc16s(temp, path, len);

        ret = _wchdir(temp);

        free(temp);
        return ret;
    }
}

FILE *
plat_fopen(const char *path, const char *mode)
{
    wchar_t *pathw;
    wchar_t *modew;
    int      len;
    FILE    *fp;

    if (acp_utf8)
        return fopen(path, mode);
    else {
        len   = mbstoc16s(NULL, path, 0) + 1;
        pathw = malloc(sizeof(wchar_t) * len);
        mbstoc16s(pathw, path, len);

        len   = mbstoc16s(NULL, mode, 0) + 1;
        modew = malloc(sizeof(wchar_t) * len);
        mbstoc16s(modew, mode, len);

        fp = _wfopen(pathw, modew);

        free(pathw);
        free(modew);

        return fp;
    }
}

/* Open a file, using Unicode pathname, with 64bit pointers. */
FILE *
plat_fopen64(const char *path, const char *mode)
{
    return plat_fopen(path, mode);
}

void
plat_remove(char *path)
{
    wchar_t *temp;
    int      len;

    if (acp_utf8)
        remove(path);
    else {
        len  = mbstoc16s(NULL, path, 0) + 1;
        temp = malloc(len * sizeof(wchar_t));
        mbstoc16s(temp, path, len);

        _wremove(temp);

        free(temp);
    }
}

void
path_normalize(UNUSED(char *path))
{
    /* No-op */
}

/* Make sure a path ends with a trailing (back)slash. */
void
path_slash(char *path)
{
    if ((path[strlen(path) - 1] != '\\') && (path[strlen(path) - 1] != '/'))
        strcat(path, "\\");
}

/* Return a trailing (back)slash if necessary. */
const char *
path_get_slash(char *path)
{
    char *ret = "";

    if ((path[strlen(path) - 1] != '\\') && (path[strlen(path) - 1] != '/'))
        ret =  "\\";

    return ret;
}

/* Check if the given path is absolute or not. */
int
path_abs(char *path)
{
    if ((path[1] == ':') || (path[0] == '\\') || (path[0] == '/'))
        return 1;

    return 0;
}

/* Return the last element of a pathname. */
char *
plat_get_basename(const char *path)
{
    int c = (int) strlen(path);

    while (c > 0) {
        if (path[c] == '/' || path[c] == '\\')
            return ((char *) &path[c + 1]);
        c--;
    }

    return ((char *) path);
}

/* Return the 'directory' element of a pathname. */
void
path_get_dirname(char *dest, const char *path)
{
    int         c = (int) strlen(path);
    const char *ptr;

    ptr = (char *) path;

    while (c > 0) {
        if (path[c] == '/' || path[c] == '\\') {
            ptr = (char *) &path[c];
            break;
        }
        c--;
    }

    /* Copy to destination. */
    while (path < ptr)
        *dest++ = *path++;
    *dest = '\0';
}

char *
path_get_filename(char *s)
{
    int c = strlen(s) - 1;

    while (c > 0) {
        if (s[c] == '/' || s[c] == '\\')
            return (&s[c + 1]);
        c--;
    }

    return s;
}

char *
path_get_extension(char *s)
{
    int c = strlen(s) - 1;

    if (c <= 0)
        return s;

    while (c && s[c] != '.')
        c--;

    if (!c)
        return (&s[strlen(s)]);

    return (&s[c + 1]);
}

void
path_append_filename(char *dest, const char *s1, const char *s2)
{
    strcpy(dest, s1);
    path_slash(dest);
    strcat(dest, s2);
}

void
plat_put_backslash(char *s)
{
    int c = strlen(s) - 1;

    if (s[c] != '/' && s[c] != '\\')
        s[c] = '/';
}

int
plat_dir_check(char *path)
{
    DWORD    dwAttrib;
    int      len;
    wchar_t *temp;

    if (acp_utf8)
        dwAttrib = GetFileAttributesA(path);
    else {
        len  = mbstoc16s(NULL, path, 0) + 1;
        temp = malloc(len * sizeof(wchar_t));
        mbstoc16s(temp, path, len);

        dwAttrib = GetFileAttributesW(temp);

        free(temp);
    }

    return ((dwAttrib != INVALID_FILE_ATTRIBUTES && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY)) ? 1 : 0);
}

int
plat_dir_create(char *path)
{
    int      ret;
    int      len;
    wchar_t *temp;

    if (acp_utf8)
        return SHCreateDirectoryExA(NULL, path, NULL);
    else {
        len  = mbstoc16s(NULL, path, 0) + 1;
        temp = malloc(len * sizeof(wchar_t));
        mbstoc16s(temp, path, len);

        ret = SHCreateDirectoryExW(NULL, temp, NULL);

        free(temp);

        return ret;
    }
}

void *
plat_mmap(size_t size, uint8_t executable)
{
    return VirtualAlloc(NULL, size, MEM_COMMIT, executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE);
}

void
plat_get_global_config_dir(char *strptr)
{
    wchar_t appdata_dir[1024] = { L'\0' };

    if (_wgetenv(L"LOCALAPPDATA") && _wgetenv(L"LOCALAPPDATA")[0] != L'\0') {
        size_t len = 0;
        wcsncpy(appdata_dir, _wgetenv(L"LOCALAPPDATA"), 1024);
        len = wcslen(appdata_dir);
        if (appdata_dir[len - 1] != L'\\') {
            appdata_dir[len]     = L'\\';
            appdata_dir[len + 1] = L'\0';
        }
        wcscat(appdata_dir, L"86box");
        CreateDirectoryW(appdata_dir, NULL);
        wcscat(appdata_dir, L"\\");
        c16stombs(strptr, appdata_dir, 1024);
    }
}

void
plat_init_rom_paths(void)
{
    wchar_t appdata_dir[1024] = { L'\0' };

    if (_wgetenv(L"LOCALAPPDATA") && _wgetenv(L"LOCALAPPDATA")[0] != L'\0') {
        char   appdata_dir_a[1024] = { '\0' };
        size_t len                 = 0;
        wcsncpy(appdata_dir, _wgetenv(L"LOCALAPPDATA"), 1024);
        len = wcslen(appdata_dir);
        if (appdata_dir[len - 1] != L'\\') {
            appdata_dir[len]     = L'\\';
            appdata_dir[len + 1] = L'\0';
        }
        wcscat(appdata_dir, L"86box");
        CreateDirectoryW(appdata_dir, NULL);
        wcscat(appdata_dir, L"\\roms");
        CreateDirectoryW(appdata_dir, NULL);
        wcscat(appdata_dir, L"\\");
        c16stombs(appdata_dir_a, appdata_dir, 1024);
        rom_add_path(appdata_dir_a);
    }
}

void
plat_munmap(void *ptr, UNUSED(size_t size))
{
    VirtualFree(ptr, 0, MEM_RELEASE);
}

uint64_t
plat_timer_read(void)
{
    LARGE_INTEGER li;

    QueryPerformanceCounter(&li);

    return (li.QuadPart);
}

static LARGE_INTEGER
plat_get_ticks_common(void)
{
    LARGE_INTEGER EndingTime;
    LARGE_INTEGER ElapsedMicroseconds;

    if (first_use) {
        QueryPerformanceFrequency(&Frequency);
        QueryPerformanceCounter(&StartingTime);
        first_use = 0;
    }

    QueryPerformanceCounter(&EndingTime);
    ElapsedMicroseconds.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;

    /* We now have the elapsed number of ticks, along with the
       number of ticks-per-second. We use these values
       to convert to the number of elapsed microseconds.
       To guard against loss-of-precision, we convert
       to microseconds *before* dividing by ticks-per-second. */
    ElapsedMicroseconds.QuadPart *= 1000000;
    ElapsedMicroseconds.QuadPart /= Frequency.QuadPart;

    return ElapsedMicroseconds;
}

uint32_t
plat_get_ticks(void)
{
    return (uint32_t) (plat_get_ticks_common().QuadPart / 1000);
}

uint32_t
plat_get_micro_ticks(void)
{
    return (uint32_t) plat_get_ticks_common().QuadPart;
}

void
plat_delay_ms(uint32_t count)
{
    Sleep(count);
}

/* Return the VIDAPI number for the given name. */
int
plat_vidapi(char *name)
{
    /* Default/System is SDL Hardware. */
    if (!strcasecmp(name, "default") || !strcasecmp(name, "system"))
        return 1;

    /* If DirectDraw or plain SDL was specified, return SDL Software. */
    if (!strcasecmp(name, "ddraw") || !strcasecmp(name, "sdl"))
        return 1;

    for (uint8_t i = 0; i < RENDERERS_NUM; i++) {
        if (vid_apis[i].name && !strcasecmp(vid_apis[i].name, name))
            return i;
    }

    /* Default value. */
    return 1;
}

/* Return the VIDAPI name for the given number. */
char *
plat_vidapi_name(int api)
{
    char *name = "default";

    switch (api) {
        case 0:
            name = "sdl_software";
            break;
        case 1:
            break;
        case 2:
            name = "sdl_opengl";
            break;
        case 3:
            name = "opengl_core";
            break;
#ifdef USE_VNC
        case 4:
            name = "vnc";
            break;
#endif
        default:
            fatal("Unknown renderer: %i\n", api);
            break;
    }

    return name;
}

int
plat_setvid(int api)
{
    int i;

    win_log("Initializing VIDAPI: api=%d\n", api);
    startblit();

    /* Close the (old) API. */
    vid_apis[vid_api].close();
    vid_api = api;

    if (vid_apis[vid_api].local)
        ShowWindow(hwndRender, SW_SHOW);
    else
        ShowWindow(hwndRender, SW_HIDE);

    /* Initialize the (new) API. */
    i = vid_apis[vid_api].init((void *) hwndRender);
    endblit();
    if (!i)
        return 0;

    device_force_redraw();

    vid_api_inited = 1;

    return 1;
}

/* Tell the renderers about a new screen resolution. */
void
plat_vidsize(int x, int y)
{
    if (!vid_api_inited || !vid_apis[vid_api].resize)
        return;

    startblit();
    vid_apis[vid_api].resize(x, y);
    endblit();
}

void
plat_vidapi_enable(int enable)
{
    int i = 1;

    if (!vid_api_inited || !vid_apis[vid_api].enable)
        return;

    vid_apis[vid_api].enable(enable != 0);

    if (!i)
        return;

    if (enable)
        device_force_redraw();
}

int
get_vidpause(void)
{
    return (vid_apis[vid_api].pause());
}

void
plat_setfullscreen(int on)
{
    RECT rect;
    int  temp_x;
    int  temp_y;
    int  dpi = win_get_dpi(hwndMain);

    /* Are we changing from the same state to the same state? */
    if ((!!(on & 1)) == (!!video_fullscreen))
        return;

    if (on && video_fullscreen_first) {
        video_fullscreen |= 2;
        if (ui_msgbox_header(MBX_INFO | MBX_DONTASK, (wchar_t *) IDS_2135, (wchar_t *) IDS_2052) == 10) {
            video_fullscreen_first = 0;
            config_save();
        }
        video_fullscreen &= 1;
    }

    /* OK, claim the video. */
    if (!(on & 2))
        win_mouse_close();

    /* Close the current mode, and open the new one. */
    video_fullscreen = (on & 1) | 2;
    if (vid_apis[vid_api].set_fs)
        vid_apis[vid_api].set_fs(on & 1);
    if (!(on & 1)) {
        plat_resize(scrnsz_x, scrnsz_y);
        if (vid_resize) {
            /* scale the screen base on DPI */
            if (!(vid_resize & 2) && window_remember) {
                MoveWindow(hwndMain, window_x, window_y, window_w, window_h, TRUE);
                GetClientRect(hwndMain, &rect);

                temp_x = rect.right - rect.left + 1;
                temp_y = rect.bottom - rect.top + 1 - (hide_status_bar ? 0 : sbar_height) - (hide_tool_bar ? 0 : tbar_height);
            } else {
                if (dpi_scale) {
                    temp_x = MulDiv((vid_resize & 2) ? fixed_size_x : unscaled_size_x, dpi, 96);
                    temp_y = MulDiv((vid_resize & 2) ? fixed_size_y : unscaled_size_y, dpi, 96);
                } else {
                    temp_x = (vid_resize & 2) ? fixed_size_x : unscaled_size_x;
                    temp_y = (vid_resize & 2) ? fixed_size_y : unscaled_size_y;
                }

                /* Main Window. */
                if (vid_resize >= 2)
                    MoveWindow(hwndMain, window_x, window_y, window_w, window_h, TRUE);

                ResizeWindowByClientArea(hwndMain, temp_x, temp_y + (hide_status_bar ? 0 : sbar_height) + (hide_tool_bar ? 0 : tbar_height));
            }

            /* Toolbar. */
            MoveWindow(hwndRebar, 0, 0, temp_x, tbar_height, TRUE);

            /* Render window. */
            MoveWindow(hwndRender, 0, hide_tool_bar ? 0 : tbar_height, temp_x, temp_y, TRUE);

            /* Status bar. */
            GetClientRect(hwndMain, &rect);
            MoveWindow(hwndSBAR, 0, rect.bottom - sbar_height, temp_x, sbar_height, TRUE);

            if (mouse_capture)
                ClipCursor(&rect);

            scrnsz_x = (vid_resize & 2) ? fixed_size_x : unscaled_size_x;
            scrnsz_y = (vid_resize & 2) ? fixed_size_y : unscaled_size_y;
        }
    }
    video_fullscreen &= 1;
    video_force_resize_set(1);
    if (!(on & 1))
        atomic_store(&doresize_monitors[0], 1);

    win_mouse_init();

    if (!(on & 2)) {
        /* Release video and make it redraw the screen. */
        device_force_redraw();

        /* Send a CTRL break code so CTRL does not get stuck. */
        keyboard_input(0, 0x01D);
    }

    /* Finally, handle the host's mouse cursor. */
    /* win_log("%s full screen, %s cursor\n", on ? "enter" : "leave", on ? "hide" : "show"); */
    show_cursor(video_fullscreen ? 0 : -1);

    if (!(on & 2)) {
        /* This is needed for OpenGL. */
        plat_vidapi_enable(0);
        plat_vidapi_enable(1);
    }
}

void
plat_vid_reload_options(void)
{
    if (!vid_api_inited || !vid_apis[vid_api].reload)
        return;

    vid_apis[vid_api].reload();
}

void
plat_vidapi_reload(void)
{
    vid_apis[vid_api].reload();
}

/* Sets up the program language before initialization. */
uint32_t
plat_language_code(char *langcode)
{
    if (!strcmp(langcode, "system"))
        return 0xFFFF;

    int      len  = mbstoc16s(NULL, langcode, 0) + 1;
    wchar_t *temp = malloc(len * sizeof(wchar_t));
    mbstoc16s(temp, langcode, len);

    LCID lcid = LocaleNameToLCID(temp, 0);

    free(temp);
    return lcid;
}

/* Converts back the language code to LCID */
void
plat_language_code_r(uint32_t lcid, char *outbuf, int len)
{
    if (lcid == 0xFFFF) {
        strcpy(outbuf, "system");
        return;
    }

    wchar_t buffer[LOCALE_NAME_MAX_LENGTH + 1];
    LCIDToLocaleName(lcid, buffer, LOCALE_NAME_MAX_LENGTH, 0);

    c16stombs(outbuf, buffer, len);
}

void
plat_get_cpu_string(char *outbuf, uint8_t len) {
    char cpu_string[] = "Unknown";
    strncpy(outbuf, cpu_string, len);
}

void
plat_set_thread_name(void *thread, const char *name)
{
    /* SetThreadDescription was added in 14393. Revisit if we ever start requiring 10. */
    static void *kernel32_handle = NULL;
    static HRESULT(WINAPI *pSetThreadDescription)(HANDLE hThread, PCWSTR lpThreadDescription) = NULL;
    static dllimp_t kernel32_imports[] = {
      // clang-format off
        { "SetThreadDescription", &pSetThreadDescription },
        { NULL,                   NULL                   }
      // clang-format on
    };

    if (!kernel32_handle) {
        kernel32_handle = dynld_module("kernel32.dll", kernel32_imports);
        if (!kernel32_handle) {
            kernel32_handle = kernel32_imports; /* store dummy pointer to avoid trying again */
            pSetThreadDescription = NULL;
        }
    }

    if (pSetThreadDescription) {
        size_t len = strlen(name) + 1;
        wchar_t wname[len + 1];
        mbstowcs(wname, name, len);
        pSetThreadDescription(thread ? (HANDLE) thread : GetCurrentThread(), wname);
    }
}

void
take_screenshot(void)
{
    startblit();
    monitors[0].mon_screenshots++;
    endblit();
    device_force_redraw();
}

/* LPARAM interface to plat_get_string(). */
LPARAM
win_get_string(int id)
{
    wchar_t *ret;

    ret = plat_get_string(id);
    return ((LPARAM) ret);
}

void /* plat_ */
startblit(void)
{
    WaitForSingleObject(ghMutex, INFINITE);
}

void /* plat_ */
endblit(void)
{
    ReleaseMutex(ghMutex);
}

double
plat_get_dpi(void)
{
    UINT dpi = win_get_dpi(hwndRender);

    return ((double) dpi) / 96.0;
}
